package com.acceptable.qucun.file.service.impl;

import com.acceptable.qucun.file.exception.FileOperationException;
import com.acceptable.qucun.generic.entity.FileItem;
import com.acceptable.qucun.generic.exception.BaseException;
import com.acceptable.qucun.generic.util.FileUtil;
import com.acceptable.qucun.generic.util.result.error.FileErrorResult;
import com.acceptable.qucun.generic.util.result.error.GenericErrorResult;
import com.acceptable.qucun.generic.util.verify.FormatVerifyUtil;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;

import java.io.*;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.FileSystemException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class FilePhysicalOperation{
    @Autowired private FileItem fileItem;

    /**
     *  文件物理上重名检测并处理
     *  @return 新的文件名 不含路径
     */
    public String getNewFileName(@NonNull String destDictPath,
                                 @NonNull String fileName,
                                 @NonNull Date date){
        if(new File(destDictPath + "/" + fileName).exists()){
            fileName = FileUtil.changeFileNameByDate(fileName, date);
        }
        return fileName;
    }

    /**
     *  文件物理上的重名检测并处理
     *  @param file 被检测文件 通常为不存在于文件所在目录的文件
     *  @return 返回File类型的检测结果
     */
    public File getNewFileName(@NonNull File file,
                               @NonNull Date date){
        String fileName = file.getName();
        if(file.exists()){
            fileName = FileUtil.changeFileNameByDate(fileName, date);
            if (!fileName.equals(file.getName())){
                file = new File(file.getParent() + "/" + fileName);
            }
        }
        return file;
    }

    /**
     *  批量删除文件
     */
    public List<File> deleteFile(@NonNull List<File> files) {
        List<File> deletedFile = new ArrayList<>();
        try {
            System.gc();
            for (File file: files){
                if (file.isDirectory()) {
                    FileUtils.deleteDirectory(file);
                }
                else{
                    FileUtils.delete(file);
                }
                deletedFile.add(file);
            }
        } catch (FileSystemException e){
            e.printStackTrace();
        } catch (IOException e){
            e.printStackTrace();
            throw new FileOperationException(FileErrorResult.PHYSICAL_DELETE_ERROR);
        }
        return deletedFile;
    }

    /**
     *  批量创建文件
     */
    public List<File> createFile(List<File> files){
        List<File> createdFile = new ArrayList<>();
        try {
            for (File file: files){
                if (file.isDirectory()){
                    file.mkdirs();
                } else
                    file.createNewFile();
                createdFile.add(file);
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new FileOperationException(FileErrorResult.PHYSICAL_CREATE_ERROR);
        }
        return createdFile;
    }

    /**
     *  查询某目录下的所有层级的所有子文件
     */
    public List<File> getAllSubFileList(@NonNull File dictFile){
        List<File> allSubFileList = new ArrayList<>();
        if (!dictFile.isDirectory() || FormatVerifyUtil.arrayIsEmpty(dictFile.listFiles())){
            allSubFileList.add(dictFile);
            return allSubFileList;
        }
        for (File subFile: dictFile.listFiles()) {
            if (subFile.isFile()) {
                allSubFileList.add(subFile);
            } else if (subFile.isDirectory()){
                allSubFileList.addAll(this.getAllSubFileList(subFile));
            }
        }
        return allSubFileList;
    }

    /**
     *  文件物理上复制/移动 若重名则依据Date进行命名
     *  复制操作
     *      目录 目标位置存在同名目录时 合并已经存在的子文件
     *      普通文件 替换
     *  移动操作
     *      目录 同上
     *      普通文件 直接抛异常
     *  借由反射调用方法
     */
    public Map<String, List<File>> copyOrMoveFile(
            @NonNull List<File> srcFileList,
            @NonNull List<File> destFileList,
            @Nullable Date date,
            @NonNull boolean isCopy){
        Map<String, List<File>> operatedFileMap = new HashMap<>();
        List<File> operatedSrcFile = new ArrayList<>();
        List<File> operatedDestFile = new ArrayList<>();
        FileUtils fileUtils = new FileUtils();
        String usage = isCopy ? "copy" : "move";
        if (srcFileList.size() != destFileList.size()){
            throw new BaseException(GenericErrorResult.DATA_LENGTH_NOT_EQUAL_ERROR);
        }

        try{
            Method dictMethod = FileUtils.class.getDeclaredMethod(usage + "Directory", File.class, File.class);
            Method fileMethod = FileUtils.class.getDeclaredMethod(usage + "File", File.class, File.class);
            for (int i = 0; i < srcFileList.size(); i++){
                File srcFile = srcFileList.get(i);
                File destFile = destFileList.get(i);

                if (date != null)
                    destFile = this.getNewFileName(destFile, date);

                if (srcFile.isDirectory()){
                    dictMethod.invoke(fileUtils, srcFile, destFile);
                } else{
                    fileMethod.invoke(fileUtils, srcFile, destFile);
                }

                operatedSrcFile.add(srcFile);
                operatedDestFile.add(destFile);
            }
            operatedFileMap.put("operatedSrcFile", operatedSrcFile);
            operatedFileMap.put("operatedDestFile", operatedDestFile);
        } catch (ReflectiveOperationException e) {
            e.printStackTrace();
            // 出错的物理还原
            if (isCopy){
                // 被删除文件无法通过创建新文件方式进行还原 需要有文件数据流
                this.deleteFile(operatedDestFile);
            } else{
                if (!FormatVerifyUtil.collectionIsEmpty(operatedDestFile) &&
                    !FormatVerifyUtil.collectionIsEmpty(operatedSrcFile)){
                    for (int i = 0; i < operatedDestFile.size(); i ++){
                        File destFile = operatedDestFile.get(i);
                        File srcFile = operatedSrcFile.get(i);
                        if (destFile.isDirectory() && srcFile.isDirectory()){
                            FileUtils.moveDirectory(destFile, srcFile);
                        } else if (destFile.isFile() && srcFile.isFile()){
                            FileUtils.moveDirectory(destFile, srcFile);
                        }
                    }
                }
            }

            FileErrorResult result = isCopy ? FileErrorResult.COPY_ERROR : FileErrorResult.MOVE_ERROR;
            throw new FileOperationException(result);
        } finally {
            return operatedFileMap;
        }
    }

    // 非反射版 -- moveFile
    /*
    public Map<String, List<File>> moveFile(@NonNull List<File> srcFileList,
                                            @NonNull List<File> destFileList,
                                            @NonNull Date date)
            throws IOException {
        Map<String, List<File>> movedFileMap = new HashMap<>();
        List<File> copiedSrcFile = new ArrayList<>();
        List<File> copiedDestFile = new ArrayList<>();
        if (srcFileList.size() != destFileList.size()){
            throw new FileOperationException(FileErrorResult.FILE_SRC_DEST_NOT_EQUAL_ERROR);
        }

        for (int i = 0; i < srcFileList.size(); i++){
            File srcFile = srcFileList.get(i);
            File destFile = destFileList.get(i);
            destFile = this.getNewFileName(destFile, date);

            if (srcFile.isDirectory()){
                FileUtils.moveDirectory(srcFile, destFile);
            } else{
                FileUtils.moveFile(srcFile, destFile);
            }
            copiedSrcFile.add(srcFile);
            copiedDestFile.add(destFile);
        }
        movedFileMap.put("movedSrcFile", copiedSrcFile);
        movedFileMap.put("movedDestFile", copiedDestFile);
        return movedFileMap;
    }

     */

    public Callable<Boolean> asyncWriteFile(@NonNull byte[] bytes,
                                            @NonNull String filePath){
        return () -> {
            // nio写入文件 异步 文件为空 待解决
            {
                ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
                buffer.put(bytes);
                RandomAccessFile accessFile = new RandomAccessFile(filePath, "rw");
                FileChannel channel = accessFile.getChannel();
                channel.write(buffer);
                channel.close();
            }
            return true;
        };
    }

    /**
     *  物理查询缓存文件
     */
    public List<File> chunkFileList(@NonNull String tmpDictPath,
                                    @NonNull String fileMd5,
                                    @NonNull Integer count){
        List<File> chunkFileList = new ArrayList<>();
        for (int i = 0; i < count; i++){
            File chunk = new File(tmpDictPath + "/" + fileMd5 + "_" + i);
            if (chunk.exists()){
                chunkFileList.add(chunk);
            }
        }
        return chunkFileList;
    }


    /**
     *  根据已得到的含有用户id的文件的路径 修改用户文件的父目录
     *  @param userId 用户id 唯一标识用户根目录(全部文件, 上传临时目录, 回收站的父目录)下的一级子目录
     *  @param newPart 使用指定字符串替换(全部文件, 上传临时目录, 回收站目录)的名称部分
     *  @param path 含有用户id以及(全部文件, 上传临时目录, 回收站目录)的目录名的路径
     */

    public String replaceRootDictInPath(@NonNull Integer userId,
                                        @NonNull String newPart,
                                        @NonNull String path){
        String prefix = "/" + userId + "/";
        String src = prefix + fileItem.getUndeletedFileDictName();
        if (!newPart.equals(fileItem.getUndeletedFileDictName())){
            path = path.replace(src, prefix + newPart);
        }
        return path;
    }


    /**
     *  与上述方法相似
     *  @param parentDictId 替换后的用户根目录的一级子目录的id 用于查找一级子目录
     */
    public String replaceRootDictInPath(@NonNull Integer userId,
                                        @NonNull long parentDictId,
                                        @NonNull String path){
        String prefix = "/" + userId + "/";
        String src = prefix + fileItem.getUndeletedFileDictName();
        if (parentDictId == fileItem.getChunkTempDictId()){
            return path.replace(src, prefix + fileItem.getChunkTempDictName());
        }

        if (parentDictId == fileItem.getDustbinDictId()){
            return path.replace(src, prefix + fileItem.getDustbinDictName());
        }
        return path;
    }


    /**
     *  普通文件或目录压缩
     *  @param srcFile 目录文件
     *  @param out 压缩文件输出流
     */
    public OutputStream compress(@NonNull File srcFile,
                                 @NonNull OutputStream out){
        ZipOutputStream resStream = null;
        try {
            String rootPath = srcFile.getAbsolutePath().replace(srcFile.getName(), "");
            resStream = new ZipOutputStream(new BufferedOutputStream(out));
            List<File> allFileList = new ArrayList<>();
            if (srcFile.isDirectory()){
                allFileList = this.getAllSubFileList(srcFile);
            } else if (srcFile.isFile()){
                allFileList.add(srcFile);
            }

            for (File file: allFileList){
                ZipEntry entry;
                String entryName = file.getAbsolutePath().replace(rootPath, "");
                if (file.isDirectory()){
                    entry = new ZipEntry(entryName+ "/");
                    resStream.putNextEntry(entry);
                } else{
                    entry = new ZipEntry(entryName);
                    resStream.putNextEntry(entry);
                    BufferedInputStream bufferIn = new BufferedInputStream(new FileInputStream(file));
                    byte[] buffer = new byte[bufferIn.available()];
                    int num = bufferIn.read(buffer);
                    bufferIn.close();
                    if (num <= 0)
                        continue;
                    resStream.write(buffer);
                }

                resStream.closeEntry();
            }

            resStream.close();
            return resStream;
        } catch (IOException e) {
            e.printStackTrace();
            throw new FileOperationException(FileErrorResult.COMPRESS_ERROR);
        }
    }

    /**
     *  文件夹压缩
     */
    public File compressDirectory(@NonNull Integer userId,
                                  @NonNull File dict)
            throws IOException{
        // 如果是文件夹则压缩成zip后传输
        String destZipPath = fileItem.getRootDictPrefix() + "/" + userId + "/" +
                fileItem.getChunkTempDictName() + "/" + dict.getName() + ".zip";
        File destFile = new File(destZipPath);
        FileOutputStream fileOut = new FileOutputStream(destFile);
        OutputStream compressFileOut = this.compress(dict, fileOut);

        // 写成压缩包后再获取输入流
        compressFileOut.flush();
        compressFileOut.close();
        return destFile;
    }

    /**
     *  获取文件二进制数据
     */
    public byte[] getFileBinaryData(@NonNull File destFile)
            throws IOException{
        InputStream in = new FileInputStream(destFile);
        byte[] arr = new byte[in.available()];
        in.read(arr);
        in.close();
        arr = Base64.getEncoder().encode(arr);
        return arr;
    }
}
